]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
remoteMenu: Port to GtkMenuTrackerItem
authorJasper St. Pierre <jstpierre@mecheye.net>
Thu, 9 May 2013 16:28:44 +0000 (12:28 -0400)
committerJasper St. Pierre <jstpierre@mecheye.net>
Mon, 13 May 2013 22:34:26 +0000 (18:34 -0400)
This pulls in new upstream API that Ryan will maintain, removing
code on our side.

Currently, our implementation of submenus will be gone, but this
will be fixed in a few commits.

https://bugzilla.gnome.org/show_bug.cgi?id=700257

js/ui/remoteMenu.js
src/Makefile.am
src/gtkmenutracker.c
src/gtkmenutracker.h
src/gtkmenutrackeritem.c [new file with mode: 0644]
src/gtkmenutrackeritem.h [new file with mode: 0644]
src/shell-menu-tracker.c
src/shell-menu-tracker.h

index 89157bfb548b4b3c023025a1dec448bd86bf2591..0e2fdf2802f2f1b617f032c68cdaa27ecccfb839 100644 (file)
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
 const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
 const Gio = imports.gi.Gio;
 const Lang = imports.lang;
 const Shell = imports.gi.Shell;
+const ShellMenu = imports.gi.ShellMenu;
 const St = imports.gi.St;
 
 const PopupMenu = imports.ui.popupMenu;
 
-/**
- * RemoteMenu:
- *
- * A PopupMenu that tracks a GMenuModel and shows its actions
- * (exposed by GApplication/GActionGroup)
- */
-const RemoteMenu = new Lang.Class({
-    Name: 'RemoteMenu',
-    Extends: PopupMenu.PopupMenu,
-
-    _init: function(sourceActor, model, actionGroup) {
-        this.parent(sourceActor, 0.0, St.Side.TOP);
-
-        this.model = model;
-        this.actionGroup = actionGroup;
-
-        this._actions = {};
-        this._trackMenu(model, this);
-
-        this._actionStateChangeId = this.actionGroup.connect('action-state-changed', Lang.bind(this, this._actionStateChanged));
-        this._actionEnableChangeId = this.actionGroup.connect('action-enabled-changed', Lang.bind(this, this._actionEnabledChanged));
-    },
-
-    destroy: function() {
-        if (this._actionStateChangeId) {
-            this.actionGroup.disconnect(this._actionStateChangeId);
-            this._actionStateChangeId = 0;
-        }
+const RemoteMenuItemMapper = new Lang.Class({
+    Name: 'RemoteMenuItemMapper',
 
-        if (this._actionEnableChangeId) {
-            this.actionGroup.disconnect(this._actionEnableChangeId);
-            this._actionEnableChangeId = 0;
-        }
+    _init: function(trackerItem) {
+        this._trackerItem = trackerItem;
 
-        this.parent();
-    },
+        this.menuItem = new PopupMenu.PopupBaseMenuItem();
+        this._label = new St.Label();
+        this.menuItem.addActor(this._label);
+        this.menuItem.actor.label_actor = this._label;
 
-    _actionAdded: function(model, item, index) {
-        let action_id = item.action_id;
-
-        if (!this._actions[action_id])
-            this._actions[action_id] = { enabled: this.actionGroup.get_action_enabled(action_id),
-                                         state: this.actionGroup.get_action_state(action_id),
-                                         items: [ ],
-                                       };
-        let action = this._actions[action_id];
-        let target, destroyId, specificSignalId;
-
-        if (action.state) {
-            // Docs have get_state_hint(), except that the DBus protocol
-            // has no provision for it (so ShellApp does not implement it,
-            // and neither GApplication), and g_action_get_state_hint()
-            // always returns null
-            // Funny :)
-
-            switch (String.fromCharCode(action.state.classify())) {
-            case 'b':
-                action.items.push(item);
-                item.setOrnament(action.state.get_boolean() ?
-                                 PopupMenu.Ornament.CHECK :
-                                 PopupMenu.Ornament.NONE);
-                specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
-                    this.actionGroup.activate_action(action_id, null);
-                }));
-                break;
-            case 's':
-                action.items.push(item);
-                item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null).deep_unpack();
-                item.setOrnament(action.state.deep_unpack() == item._remoteTarget ?
-                                 PopupMenu.Ornament.DOT :
-                                 PopupMenu.Ornament.NONE);
-                specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
-                    this.actionGroup.activate_action(action_id, GLib.Variant.new_string(item._remoteTarget));
-                }));
-                break;
-            default:
-                log('Action "%s" has state of type %s, which is not supported'.format(action_id, action.state.get_type_string()));
-                return;
-            }
-        } else {
-            target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null);
-            action.items.push(item);
-            specificSignalId = item.connect('activate', Lang.bind(this, function() {
-                this.actionGroup.activate_action(action_id, target);
-            }));
-        }
-
-        item.actor.reactive = item.actor.can_focus = action.enabled;
+        this.menuItem.connect('activate', Lang.bind(this, function() {
+            this._trackerItem.activated();
+        }));
 
-        destroyId = item.connect('destroy', Lang.bind(this, function() {
-            item.disconnect(destroyId);
-            item.disconnect(specificSignalId);
+        this._trackerItem.bind_property('visible', this.menuItem.actor, 'visible', GObject.BindingFlags.SYNC_CREATE);
 
-            let pos = action.items.indexOf(item);
-            if (pos != -1)
-                action.items.splice(pos, 1);
-        }));
-    },
+        this._trackerItem.connect('notify::label', Lang.bind(this, this._updateLabel));
+        this._trackerItem.connect('notify::sensitive', Lang.bind(this, this._updateSensitivity));
+        this._trackerItem.connect('notify::role', Lang.bind(this, this._updateDecoration));
+        this._trackerItem.connect('notify::toggled', Lang.bind(this, this._updateDecoration));
 
-    _trackMenu: function(model, item) {
-        item._tracker = Shell.MenuTracker.new(model,
-                                              null, /* action namespace */
-                                              Lang.bind(this, this._insertItem, item),
-                                              Lang.bind(this, this._removeItem, item));
+        this._updateLabel();
+        this._updateSensitivity();
+        this._updateDecoration();
 
-        item.connect('destroy', function() {
-            item._tracker.destroy();
-            item._tracker = null;
+        this.menuItem.connect('destroy', function() {
+            trackerItem.run_dispose();
         });
     },
 
-    _createMenuItem: function(model, index) {
-        let labelValue = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null);
-        let label = labelValue ? labelValue.deep_unpack() : '';
+    _updateLabel: function() {
+        let label = this._trackerItem.label;
         // remove all underscores that are not followed by another underscore
         label = label.replace(/_([^_])/, '$1');
+        this._label.text = label;
+    },
 
-        let submenuModel = model.get_item_link(index, Gio.MENU_LINK_SUBMENU);
-        if (submenuModel) {
-            let item = new PopupMenu.PopupSubMenuMenuItem(label);
-            this._trackMenu(submenuModel, item.menu);
-            return item;
-        }
-
-        let item = new PopupMenu.PopupMenuItem(label);
-        let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack();
-        item.actor.can_focus = item.actor.reactive = false;
+    _updateSensitivity: function() {
+        this.menuItem.setSensitive(this._trackerItem.sensitive);
+    },
 
-        item.action_id = action_id;
+    _updateDecoration: function() {
+        let ornamentForRole = {};
+        ornamentForRole[ShellMenu.MenuTrackerItemRole.RADIO] = PopupMenu.Ornament.DOT;
+        ornamentForRole[ShellMenu.MenuTrackerItemRole.CHECK] = PopupMenu.Ornament.CHECK;
 
-        if (this.actionGroup.has_action(action_id)) {
-            this._actionAdded(model, item, index);
-            return item;
-        }
+        let ornament = PopupMenu.Ornament.NONE;
+        if (this._trackerItem.toggled)
+            ornament = ornamentForRole[this._trackerItem.role];
 
-        let signalId = this.actionGroup.connect('action-added', Lang.bind(this, function(actionGroup, actionName) {
-            actionGroup.disconnect(signalId);
-            if (this._actions[actionName]) return;
+        this.menuItem.setOrnament(ornament);
+    },
+});
 
-            this._actionAdded(model, item, index);
-        }));
+const RemoteMenu = new Lang.Class({
+    Name: 'RemoteMenu',
+    Extends: PopupMenu.PopupMenu,
 
-        return item;
-    },
+    _init: function(sourceActor, model, actionGroup) {
+        this.parent(sourceActor, 0.0, St.Side.TOP);
 
-    _actionStateChanged: function(actionGroup, action_id) {
-        let action = this._actions[action_id];
-        if (!action)
-            return;
-
-        action.state = actionGroup.get_action_state(action_id);
-        if (action.items.length) {
-            switch (String.fromCharCode(action.state.classify())) {
-            case 'b':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setOrnament(action.state.get_boolean() ?
-                                                Ornament.CHECK : Ornament.NONE);
-                break;
-            case 'd':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setValue(action.state.get_double());
-                break;
-            case 's':
-                for (let i = 0; i < action.items.length; i++)
-                    action.items[i].setOrnament(action.items[i]._remoteTarget == action.state.deep_unpack() ?
-                                                Ornament.DOT : Ornament.NONE);
-            }
-        }
+        this._model = model;
+        this._actionGroup = actionGroup;
+        this._tracker = Shell.MenuTracker.new(this._actionGroup,
+                                              this._model,
+                                              null, /* action namespace */
+                                              Lang.bind(this, this._insertItem),
+                                              Lang.bind(this, this._removeItem));
     },
 
-    _actionEnabledChanged: function(actionGroup, action_id) {
-        let action = this._actions[action_id];
-        if (!action)
-            return;
-
-        action.enabled = actionGroup.get_action_enabled(action_id);
-        if (action.items.length) {
-            for (let i = 0; i < action.items.length; i++) {
-                let item = action.items[i];
-                item.actor.reactive = item.actor.can_focus = action.enabled;
-            }
-        }
+    destroy: function() {
+        this._tracker.destroy();
+        this.parent();
     },
 
-    _insertItem: function(position, model, item_index, action_namespace, is_separator, target) {
+    _insertItem: function(trackerItem, position) {
         let item;
 
-        if (is_separator)
+        if (trackerItem.get_is_separator()) {
             item = new PopupMenu.PopupSeparatorMenuItem();
-        else
-            item = this._createMenuItem(model, item_index);
+        } else {
+            let mapper = new RemoteMenuItemMapper(trackerItem);
+            item = mapper.menuItem;
+        }
 
-        target.addMenuItem(item, position);
+        this.addMenuItem(item, position);
     },
 
-    _removeItem: function(position, target) {
-        let items = target._getMenuItems();
+    _removeItem: function(position) {
+        let items = this._getMenuItems();
         items[position].destroy();
     },
 });
index e53825d3de4d352cf07dde447fb07dc98a37e234..c0e370efcbfc7b05b38de124bf5419ac1ed1381c 100644 (file)
@@ -136,6 +136,8 @@ shell_private_sources =             \
        gtkactionobservable.c           \
        gtkactionobserver.h             \
        gtkactionobserver.c             \
+       gtkmenutrackeritem.c            \
+       gtkmenutrackeritem.h            \
        gtkmenutracker.c                \
        gtkmenutracker.h                \
        $(NULL)
@@ -304,6 +306,8 @@ ShellMenu_0_1_gir_FILES = \
        gtkactionobservable.c           \
        gtkactionobserver.h             \
        gtkactionobserver.c             \
+       gtkmenutrackeritem.c            \
+       gtkmenutrackeritem.h            \
        $(NULL)
 ShellMenu_0_1_gir_SCANNERFLAGS = --namespace=ShellMenu --identifier-prefix=Gtk
 INTROSPECTION_GIRS += ShellMenu-0.1.gir
index f49457f115876c4bd33c83e1350066b8a875a64e..ab369ab02f9d6059d3ec65b32e8400ea5bcff4c8 100644 (file)
 
 #include "gtkmenutracker.h"
 
+/**
+ * SECTION:gtkmenutracker
+ * @Title: GtkMenuTracker
+ * @Short_description: A helper class for interpreting #GMenuModel
+ *
+ * #GtkMenuTracker is a simple object to ease implementations of #GMenuModel.
+ * Given a #GtkActionObservable (usually a #GActionMuxer) along with a
+ * #GMenuModel, it will tell you which menu items to create and where to place
+ * them. If a menu item is removed, it will tell you the position of the menu
+ * item to remove.
+ *
+ * Using #GtkMenuTracker is fairly simple. The only guarantee you must make
+ * to #GtkMenuTracker is that you must obey all insert signals and track the
+ * position of items that #GtkMenuTracker gives you. That is, #GtkMenuTracker
+ * expects positions of all the latter items to change when it calls your
+ * insertion callback with an early position, as it may ask you to remove
+ * an item with a readjusted position later.
+ *
+ * #GtkMenuTracker will give you a #GtkMenuTrackerItem in your callback. You
+ * must hold onto this object until a remove signal is emitted. This item
+ * represents a single menu item, which can be one of three classes: normal item,
+ * separator, or submenu.
+ *
+ * Certain properties on the #GtkMenuTrackerItem are mutable, and you must
+ * listen for changes in the item. For more details, see the documentation
+ * for #GtkMenuTrackerItem along with https://live.gnome.org/GApplication/GMenuModel.
+ *
+ * The idea of @with_separators is for special cases where menu models may
+ * be tracked in places where separators are not available, like in toplevel
+ * "File", "Edit" menu bars. Ignoring separator items is wrong, as #GtkMenuTracker
+ * expects the position to change, so we must tell #GtkMenuTracker to ignore
+ * separators itself.
+ */
+
 typedef struct _GtkMenuTrackerSection GtkMenuTrackerSection;
 
 struct _GtkMenuTracker
 {
+  GtkActionObservable      *observable;
   GtkMenuTrackerInsertFunc  insert_func;
   GtkMenuTrackerRemoveFunc  remove_func;
   gpointer                  user_data;
@@ -159,7 +194,12 @@ gtk_menu_tracker_section_sync_separators (GtkMenuTrackerSection *section,
   if (should_have_separator > section->has_separator)
     {
       /* Add a separator */
-      (* tracker->insert_func) (offset, parent_model, parent_index, NULL, TRUE, tracker->user_data);
+      GtkMenuTrackerItem *item;
+
+      item = _gtk_menu_tracker_item_new (tracker->observable, parent_model, parent_index, NULL, TRUE);
+      (* tracker->insert_func) (item, offset, tracker->user_data);
+      g_object_unref (item);
+
       section->has_separator = TRUE;
     }
   else if (should_have_separator < section->has_separator)
@@ -258,8 +298,13 @@ gtk_menu_tracker_add_items (GtkMenuTracker         *tracker,
         }
       else
         {
-          (* tracker->insert_func) (offset, model, position + n_items,
-                                    section->action_namespace, FALSE, tracker->user_data);
+          GtkMenuTrackerItem *item;
+
+          item = _gtk_menu_tracker_item_new (tracker->observable, model, position + n_items,
+                                             section->action_namespace, FALSE);
+          (* tracker->insert_func) (item, offset, tracker->user_data);
+          g_object_unref (item);
+
           *change_point = g_slist_prepend (*change_point, NULL);
         }
     }
@@ -400,7 +445,8 @@ gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
  * gtk_menu_tracker_free() is called.
  */
 GtkMenuTracker *
-gtk_menu_tracker_new (GMenuModel               *model,
+gtk_menu_tracker_new (GtkActionObservable      *observable,
+                      GMenuModel               *model,
                       gboolean                  with_separators,
                       const gchar              *action_namespace,
                       GtkMenuTrackerInsertFunc  insert_func,
@@ -410,6 +456,7 @@ gtk_menu_tracker_new (GMenuModel               *model,
   GtkMenuTracker *tracker;
 
   tracker = g_slice_new (GtkMenuTracker);
+  tracker->observable = g_object_ref (observable);
   tracker->insert_func = insert_func;
   tracker->remove_func = remove_func;
   tracker->user_data = user_data;
@@ -420,6 +467,19 @@ gtk_menu_tracker_new (GMenuModel               *model,
   return tracker;
 }
 
+GtkMenuTracker *
+gtk_menu_tracker_new_for_item_submenu (GtkMenuTrackerItem       *item,
+                                       GtkMenuTrackerInsertFunc  insert_func,
+                                       GtkMenuTrackerRemoveFunc  remove_func,
+                                       gpointer                  user_data)
+{
+  return gtk_menu_tracker_new (_gtk_menu_tracker_item_get_observable (item),
+                               _gtk_menu_tracker_item_get_submenu (item),
+                               TRUE,
+                               _gtk_menu_tracker_item_get_submenu_namespace (item),
+                               insert_func, remove_func, user_data);
+}
+
 /*< private >
  * gtk_menu_tracker_free:
  * @tracker: a #GtkMenuTracker
@@ -430,5 +490,6 @@ void
 gtk_menu_tracker_free (GtkMenuTracker *tracker)
 {
   gtk_menu_tracker_section_free (tracker->toplevel);
+  g_object_unref (tracker->observable);
   g_slice_free (GtkMenuTracker, tracker);
 }
index 51810a104c5e36a955b94f1e6520b7b36f784a7b..96370adcb8fa17d99c5cc8ce9e1a4a5231babc8a 100644 (file)
 #ifndef __GTK_MENU_TRACKER_H__
 #define __GTK_MENU_TRACKER_H__
 
-#include <gio/gio.h>
+#include "gtkmenutrackeritem.h"
 
 typedef struct _GtkMenuTracker GtkMenuTracker;
 
-typedef void         (* GtkMenuTrackerInsertFunc)       (gint                      position,
-                                                         GMenuModel               *model,
-                                                         gint                      item_index,
-                                                         const gchar              *action_namespace,
-                                                         gboolean                  is_separator,
-                                                         gpointer                  user_data);
+typedef void         (* GtkMenuTrackerInsertFunc)                       (GtkMenuTrackerItem       *item,
+                                                                         gint                      position,
+                                                                         gpointer                  user_data);
 
-typedef void         (* GtkMenuTrackerRemoveFunc)       (gint                      position,
-                                                         gpointer                  user_data);
+typedef void         (* GtkMenuTrackerRemoveFunc)                       (gint                      position,
+                                                                         gpointer                  user_data);
 
 
-G_GNUC_INTERNAL
-GtkMenuTracker *        gtk_menu_tracker_new            (GMenuModel               *model,
-                                                         gboolean                  with_separators,
-                                                         const gchar              *action_namespace,
-                                                         GtkMenuTrackerInsertFunc  insert_func,
-                                                         GtkMenuTrackerRemoveFunc  remove_func,
-                                                         gpointer                  user_data);
+GtkMenuTracker *        gtk_menu_tracker_new                            (GtkActionObservable      *observer,
+                                                                         GMenuModel               *model,
+                                                                         gboolean                  with_separators,
+                                                                         const gchar              *action_namespace,
+                                                                         GtkMenuTrackerInsertFunc  insert_func,
+                                                                         GtkMenuTrackerRemoveFunc  remove_func,
+                                                                         gpointer                  user_data);
 
-G_GNUC_INTERNAL
-void                    gtk_menu_tracker_free           (GtkMenuTracker           *tracker);
+GtkMenuTracker *        gtk_menu_tracker_new_for_item_submenu           (GtkMenuTrackerItem       *item,
+                                                                         GtkMenuTrackerInsertFunc  insert_func,
+                                                                         GtkMenuTrackerRemoveFunc  remove_func,
+                                                                         gpointer                  user_data);
+
+void                    gtk_menu_tracker_free                           (GtkMenuTracker           *tracker);
 
 #endif /* __GTK_MENU_TRACKER_H__ */
diff --git a/src/gtkmenutrackeritem.c b/src/gtkmenutrackeritem.c
new file mode 100644 (file)
index 0000000..2d712a1
--- /dev/null
@@ -0,0 +1,788 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "gtkmenutrackeritem.h"
+
+/**
+ * SECTION:gtkmenutrackeritem
+ * @Title: GtkMenuTrackerItem
+ * @Short_description: Small helper for model menu items
+ *
+ * A #GtkMenuTrackerItem is a small helper class used by #GtkMenuTracker to
+ * represent menu items. It has one of three classes: normal item, separator,
+ * or submenu.
+ *
+ * If an item is one of the non-normal classes (submenu, separator), only the
+ * label of the item needs to be respected. Otherwise, all the properties
+ * of the item contribute to the item's appearance and state.
+ *
+ * Implementing the appearance of the menu item is up to toolkits, and certain
+ * toolkits may choose to ignore certain properties, like icon or accel. The
+ * role of the item determines its accessibility role, along with its
+ * decoration if the GtkMenuTrackerItem::toggled property is true. As an
+ * example, if the item has the role %GTK_MENU_TRACKER_ITEM_ROLE_CHECK and
+ * GtkMenuTrackerItem::toggled is %FALSE, its accessible role should be that of
+ * a check menu item, and no decoration should be drawn. But if
+ * GtkMenuTrackerItem::toggled is %TRUE, a checkmark should be drawn.
+ *
+ * All properties except for the two class-determining properties,
+ * GtkMenuTrackerItem::is-separator and GtkMenuTrackerItem::has-submenu are
+ * allowed to change, so listen to the notify signals to update your item's
+ * appearance. When using a GObject library, this can conveniently be done
+ * with g_object_bind_property() and #GBinding, and this is how this is
+ * implemented in GTK+; the appearance side is implemented in #GtkModelMenuItem.
+ *
+ * When an item is clicked, simply call gtk_menu_tracker_item_activated() in
+ * response. The #GtkMenuTrackerItem will take care of everything related to
+ * activating the item and will itself update the state of all items in
+ * response.
+ *
+ * Submenus are a special case of menu item. When an item is a submenu, you
+ * should create a submenu for it with gtk_menu_tracker_new_item_for_submenu(),
+ * and apply the same menu tracking logic you would for a toplevel menu.
+ * Applications using submenus may want to lazily build their submenus in
+ * response to the user clicking on it, as building a submenu may be expensive.
+ *
+ * Thus, the submenu has two special controls -- the submenu's visibility
+ * should be controlled by the GtkMenuTrackerItem::submenu-shown property,
+ * and if a user clicks on the submenu, do not immediately show the menu,
+ * but call gtk_menu_tracker_item_request_submenu_shown() and wait for the
+ * GtkMenuTrackerItem::submenu-shown property to update. If the user navigates,
+ * the application may want to be notified so it can cancel the expensive
+ * operation that it was using to build the submenu. Thus,
+ * gtk_menu_tracker_item_request_submenu_shown() takes a boolean parameter.
+ * Use %TRUE when the user wants to open the submenu, and %FALSE when the
+ * user wants to close the submenu.
+ */
+
+typedef GObjectClass GtkMenuTrackerItemClass;
+
+struct _GtkMenuTrackerItem
+{
+  GObject parent_instance;
+
+  GtkActionObservable *observable;
+  gchar *action_namespace;
+  GMenuItem *item;
+  GtkMenuTrackerItemRole role : 4;
+  guint is_separator : 1;
+  guint can_activate : 1;
+  guint sensitive : 1;
+  guint toggled : 1;
+  guint submenu_shown : 1;
+  guint submenu_requested : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_IS_SEPARATOR,
+  PROP_HAS_SUBMENU,
+  PROP_LABEL,
+  PROP_ICON,
+  PROP_SENSITIVE,
+  PROP_VISIBLE,
+  PROP_ROLE,
+  PROP_TOGGLED,
+  PROP_ACCEL,
+  PROP_SUBMENU_SHOWN,
+  N_PROPS
+};
+
+static GParamSpec *gtk_menu_tracker_item_pspecs[N_PROPS];
+
+static void gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface);
+G_DEFINE_TYPE_WITH_CODE (GtkMenuTrackerItem, gtk_menu_tracker_item, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, gtk_menu_tracker_item_init_observer_iface))
+
+GType
+gtk_menu_tracker_item_role_get_type (void)
+{
+  static gsize gtk_menu_tracker_item_role_type;
+
+  if (g_once_init_enter (&gtk_menu_tracker_item_role_type))
+    {
+      static const GEnumValue values[] = {
+        { GTK_MENU_TRACKER_ITEM_ROLE_NORMAL, "GTK_MENU_TRACKER_ITEM_ROLE_NORMAL", "normal" },
+        { GTK_MENU_TRACKER_ITEM_ROLE_CHECK, "GTK_MENU_TRACKER_ITEM_ROLE_CHECK", "check" },
+        { GTK_MENU_TRACKER_ITEM_ROLE_RADIO, "GTK_MENU_TRACKER_ITEM_ROLE_RADIO", "radio" },
+        { 0, NULL, NULL }
+      };
+      GType type;
+
+      type = g_enum_register_static ("GtkMenuTrackerItemRole", values);
+
+      g_once_init_leave (&gtk_menu_tracker_item_role_type, type);
+    }
+
+  return gtk_menu_tracker_item_role_type;
+}
+
+static void
+gtk_menu_tracker_item_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_SEPARATOR:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_is_separator (self));
+      break;
+    case PROP_HAS_SUBMENU:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_has_submenu (self));
+      break;
+    case PROP_LABEL:
+      g_value_set_string (value, gtk_menu_tracker_item_get_label (self));
+      break;
+    case PROP_ICON:
+      g_value_set_object (value, gtk_menu_tracker_item_get_icon (self));
+      break;
+    case PROP_SENSITIVE:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_sensitive (self));
+      break;
+    case PROP_VISIBLE:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_visible (self));
+      break;
+    case PROP_ROLE:
+      g_value_set_enum (value, gtk_menu_tracker_item_get_role (self));
+      break;
+    case PROP_TOGGLED:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_toggled (self));
+      break;
+    case PROP_ACCEL:
+      g_value_set_string (value, gtk_menu_tracker_item_get_accel (self));
+      break;
+    case PROP_SUBMENU_SHOWN:
+      g_value_set_boolean (value, gtk_menu_tracker_item_get_submenu_shown (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+  }
+}
+
+static void
+gtk_menu_tracker_item_finalize (GObject *object)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
+
+  g_free (self->action_namespace);
+
+  if (self->observable)
+    g_object_unref (self->observable);
+
+  g_object_unref (self->item);
+
+  G_OBJECT_CLASS (gtk_menu_tracker_item_parent_class)->finalize (object);
+}
+
+static void
+gtk_menu_tracker_item_init (GtkMenuTrackerItem * self)
+{
+}
+
+static void
+gtk_menu_tracker_item_class_init (GtkMenuTrackerItemClass *class)
+{
+  class->get_property = gtk_menu_tracker_item_get_property;
+  class->finalize = gtk_menu_tracker_item_finalize;
+
+  gtk_menu_tracker_item_pspecs[PROP_IS_SEPARATOR] =
+    g_param_spec_boolean ("is-separator", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_HAS_SUBMENU] =
+    g_param_spec_boolean ("has-submenu", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_LABEL] =
+    g_param_spec_string ("label", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ICON] =
+    g_param_spec_object ("icon", "", "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_SENSITIVE] =
+    g_param_spec_boolean ("sensitive", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_VISIBLE] =
+    g_param_spec_boolean ("visible", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ROLE] =
+    g_param_spec_enum ("role", "", "",
+                       GTK_TYPE_MENU_TRACKER_ITEM_ROLE, GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+                       G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_TOGGLED] =
+    g_param_spec_boolean ("toggled", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_ACCEL] =
+    g_param_spec_string ("accel", "", "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+  gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN] =
+    g_param_spec_boolean ("submenu-shown", "", "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+
+  g_object_class_install_properties (class, N_PROPS, gtk_menu_tracker_item_pspecs);
+}
+
+static void
+gtk_menu_tracker_item_action_added (GtkActionObserver   *observer,
+                                    GtkActionObservable *observable,
+                                    const gchar         *action_name,
+                                    const GVariantType  *parameter_type,
+                                    gboolean             enabled,
+                                    GVariant            *state)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+  GVariant *action_target;
+
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+  self->can_activate = (action_target == NULL && parameter_type == NULL) ||
+                        (action_target != NULL && parameter_type != NULL &&
+                        g_variant_is_of_type (action_target, parameter_type));
+
+  if (!self->can_activate)
+    {
+      if (action_target)
+        g_variant_unref (action_target);
+      return;
+    }
+
+  self->sensitive = enabled;
+
+  if (action_target != NULL && state != NULL)
+    {
+      self->toggled = g_variant_equal (state, action_target);
+      self->role = GTK_MENU_TRACKER_ITEM_ROLE_RADIO;
+    }
+
+  else if (state != NULL && g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+    {
+      self->toggled = g_variant_get_boolean (state);
+      self->role = GTK_MENU_TRACKER_ITEM_ROLE_CHECK;
+    }
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (self->sensitive)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+
+  if (self->toggled)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+
+  if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+
+  if (action_target)
+    g_variant_unref (action_target);
+}
+
+static void
+gtk_menu_tracker_item_action_enabled_changed (GtkActionObserver   *observer,
+                                              GtkActionObservable *observable,
+                                              const gchar         *action_name,
+                                              gboolean             enabled)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+
+  if (!self->can_activate)
+    return;
+
+  if (self->sensitive == enabled)
+    return;
+
+  self->sensitive = enabled;
+
+  g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+}
+
+static void
+gtk_menu_tracker_item_action_state_changed (GtkActionObserver   *observer,
+                                            GtkActionObservable *observable,
+                                            const gchar         *action_name,
+                                            GVariant            *state)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+  GVariant *action_target;
+  gboolean was_toggled;
+
+  if (!self->can_activate)
+    return;
+
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+  was_toggled = self->toggled;
+
+  if (action_target)
+    {
+      self->toggled = g_variant_equal (state, action_target);
+      g_variant_unref (action_target);
+    }
+
+  else if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+    self->toggled = g_variant_get_boolean (state);
+
+  else
+    self->toggled = FALSE;
+
+  if (self->toggled != was_toggled)
+    g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+}
+
+static void
+gtk_menu_tracker_item_action_removed (GtkActionObserver   *observer,
+                                      GtkActionObservable *observable,
+                                      const gchar         *action_name)
+{
+  GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
+
+  if (!self->can_activate)
+    return;
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (self->sensitive)
+    {
+      self->sensitive = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
+    }
+
+  if (self->toggled)
+    {
+      self->toggled = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
+    }
+
+  if (self->role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
+    {
+      self->role = GTK_MENU_TRACKER_ITEM_ROLE_NORMAL;
+      g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_ROLE]);
+    }
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface)
+{
+  iface->action_added = gtk_menu_tracker_item_action_added;
+  iface->action_enabled_changed = gtk_menu_tracker_item_action_enabled_changed;
+  iface->action_state_changed = gtk_menu_tracker_item_action_state_changed;
+  iface->action_removed = gtk_menu_tracker_item_action_removed;
+}
+
+GtkMenuTrackerItem *
+_gtk_menu_tracker_item_new (GtkActionObservable *observable,
+                            GMenuModel          *model,
+                            gint                 item_index,
+                            const gchar         *action_namespace,
+                            gboolean             is_separator)
+{
+  GtkMenuTrackerItem *self;
+  const gchar *action_name;
+
+  g_return_val_if_fail (GTK_IS_ACTION_OBSERVABLE (observable), NULL);
+  g_return_val_if_fail (G_IS_MENU_MODEL (model), NULL);
+
+  self = g_object_new (GTK_TYPE_MENU_TRACKER_ITEM, NULL);
+  self->item = g_menu_item_new_from_model (model, item_index);
+  self->action_namespace = g_strdup (action_namespace);
+  self->observable = g_object_ref (observable);
+  self->is_separator = is_separator;
+
+  if (!is_separator && g_menu_item_get_attribute (self->item, "action", "&s", &action_name))
+    {
+      GActionGroup *group = G_ACTION_GROUP (observable);
+      const GVariantType *parameter_type;
+      gboolean enabled;
+      GVariant *state;
+      gboolean found;
+
+      state = NULL;
+
+      if (action_namespace)
+        {
+          gchar *full_action;
+
+          full_action = g_strjoin (".", action_namespace, action_name, NULL);
+          gtk_action_observable_register_observer (self->observable, full_action, GTK_ACTION_OBSERVER (self));
+          found = g_action_group_query_action (group, full_action, &enabled, &parameter_type, NULL, NULL, &state);
+          g_free (full_action);
+        }
+      else
+        {
+          gtk_action_observable_register_observer (self->observable, action_name, GTK_ACTION_OBSERVER (self));
+          found = g_action_group_query_action (group, action_name, &enabled, &parameter_type, NULL, NULL, &state);
+        }
+
+      if (found)
+        gtk_menu_tracker_item_action_added (GTK_ACTION_OBSERVER (self), observable, NULL, parameter_type, enabled, state);
+      else
+        gtk_menu_tracker_item_action_removed (GTK_ACTION_OBSERVER (self), observable, NULL);
+
+      if (state)
+        g_variant_unref (state);
+    }
+  else
+    self->sensitive = TRUE;
+
+  return self;
+}
+
+GtkActionObservable *
+_gtk_menu_tracker_item_get_observable (GtkMenuTrackerItem *self)
+{
+  return self->observable;
+}
+
+/**
+ * gtk_menu_tracker_item_get_is_separator:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item is a separator. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_is_separator (GtkMenuTrackerItem *self)
+{
+  return self->is_separator;
+}
+
+/**
+ * gtk_menu_tracker_item_get_has_submenu:
+ * @self: A #GtkMenuTrackerItem instance
+ *
+ * Returns whether the menu item has a submenu. If so, only
+ * certain properties may need to be obeyed. See the documentation
+ * for #GtkMenuTrackerItem.
+ */
+gboolean
+gtk_menu_tracker_item_get_has_submenu (GtkMenuTrackerItem *self)
+{
+  GMenuModel *link;
+
+  link = g_menu_item_get_link (self->item, G_MENU_LINK_SUBMENU);
+
+  if (link)
+    {
+      g_object_unref (link);
+      return TRUE;
+    }
+  else
+    return FALSE;
+}
+
+const gchar *
+gtk_menu_tracker_item_get_label (GtkMenuTrackerItem *self)
+{
+  const gchar *label = NULL;
+
+  g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_LABEL, "&s", &label);
+
+  return label;
+}
+
+/**
+ * gtk_menu_tracker_item_get_icon:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+gtk_menu_tracker_item_get_icon (GtkMenuTrackerItem *self)
+{
+  GVariant *icon_data;
+  GIcon *icon;
+
+  icon_data = g_menu_item_get_attribute_value (self->item, "icon", NULL);
+
+  if (icon_data == NULL)
+    return NULL;
+
+  icon = g_icon_deserialize (icon_data);
+  g_variant_unref (icon_data);
+
+  return icon;
+}
+
+gboolean
+gtk_menu_tracker_item_get_sensitive (GtkMenuTrackerItem *self)
+{
+  return self->sensitive;
+}
+
+gboolean
+gtk_menu_tracker_item_get_visible (GtkMenuTrackerItem *self)
+{
+  return TRUE;
+}
+
+GtkMenuTrackerItemRole
+gtk_menu_tracker_item_get_role (GtkMenuTrackerItem *self)
+{
+  return self->role;
+}
+
+gboolean
+gtk_menu_tracker_item_get_toggled (GtkMenuTrackerItem *self)
+{
+  return self->toggled;
+}
+
+const gchar *
+gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self)
+{
+  const gchar *accel = NULL;
+
+  g_menu_item_get_attribute (self->item, "accel", "&s", &accel);
+
+  return accel;
+}
+
+GMenuModel *
+_gtk_menu_tracker_item_get_submenu (GtkMenuTrackerItem *self)
+{
+  return g_menu_item_get_link (self->item, "submenu");
+}
+
+gchar *
+_gtk_menu_tracker_item_get_submenu_namespace (GtkMenuTrackerItem *self)
+{
+  const gchar *namespace;
+
+  if (g_menu_item_get_attribute (self->item, "action-namespace", "&s", &namespace))
+    {
+      if (self->action_namespace)
+        return g_strjoin (".", self->action_namespace, namespace, NULL);
+      else
+        return g_strdup (namespace);
+    }
+  else
+    return g_strdup (self->action_namespace);
+}
+
+gboolean
+gtk_menu_tracker_item_get_should_request_show (GtkMenuTrackerItem *self)
+{
+  return g_menu_item_get_attribute (self->item, "submenu-action", "&s", NULL);
+}
+
+gboolean
+gtk_menu_tracker_item_get_submenu_shown (GtkMenuTrackerItem *self)
+{
+  return self->submenu_shown;
+}
+
+static void
+gtk_menu_tracker_item_set_submenu_shown (GtkMenuTrackerItem *self,
+                                         gboolean            submenu_shown)
+{
+  if (submenu_shown == self->submenu_shown)
+    return;
+
+  self->submenu_shown = submenu_shown;
+  g_object_notify_by_pspec (G_OBJECT (self), gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN]);
+}
+
+void
+gtk_menu_tracker_item_activated (GtkMenuTrackerItem *self)
+{
+  const gchar *action_name;
+  GVariant *action_target;
+
+  g_return_if_fail (GTK_IS_MENU_TRACKER_ITEM (self));
+
+  if (!self->can_activate)
+    return;
+
+  g_menu_item_get_attribute (self->item, G_MENU_ATTRIBUTE_ACTION, "&s", &action_name);
+  action_target = g_menu_item_get_attribute_value (self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
+
+  if (self->action_namespace)
+    {
+      gchar *full_action;
+
+      full_action = g_strjoin (".", self->action_namespace, action_name, NULL);
+      g_action_group_activate_action (G_ACTION_GROUP (self->observable), full_action, action_target);
+      g_free (full_action);
+    }
+  else
+    g_action_group_activate_action (G_ACTION_GROUP (self->observable), action_name, action_target);
+
+  if (action_target)
+    g_variant_unref (action_target);
+}
+
+typedef struct
+{
+  GtkMenuTrackerItem *item;
+  gchar              *submenu_action;
+  gboolean            first_time;
+} GtkMenuTrackerOpener;
+
+static void
+gtk_menu_tracker_opener_update (GtkMenuTrackerOpener *opener)
+{
+  GActionGroup *group = G_ACTION_GROUP (opener->item->observable);
+  gboolean is_open = TRUE;
+
+  /* We consider the menu as being "open" if the action does not exist
+   * or if there is another problem (no state, wrong state type, etc.).
+   * If the action exists, with the correct state then we consider it
+   * open if we have ever seen this state equal to TRUE.
+   *
+   * In the event that we see the state equal to FALSE, we force it back
+   * to TRUE.  We do not signal that the menu was closed because this is
+   * likely to create UI thrashing.
+   *
+   * The only way the menu can have a true-to-false submenu-shown
+   * transition is if the user calls _request_submenu_shown (FALSE).
+   * That is handled in _free() below.
+   */
+
+  if (g_action_group_has_action (group, opener->submenu_action))
+    {
+      GVariant *state = g_action_group_get_action_state (group, opener->submenu_action);
+
+      if (state)
+        {
+          if (g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+            is_open = g_variant_get_boolean (state);
+          g_variant_unref (state);
+        }
+    }
+
+  /* If it is already open, signal that.
+   *
+   * If it is not open, ask it to open.
+   */
+  if (is_open)
+    gtk_menu_tracker_item_set_submenu_shown (opener->item, TRUE);
+
+  if (!is_open || opener->first_time)
+    {
+      g_action_group_change_action_state (group, opener->submenu_action, g_variant_new_boolean (TRUE));
+      opener->first_time = FALSE;
+    }
+}
+
+static void
+gtk_menu_tracker_opener_added (GActionGroup *group,
+                               const gchar  *action_name,
+                               gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_removed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_changed (GActionGroup *action_group,
+                                 const gchar  *action_name,
+                                 GVariant     *new_state,
+                                 gpointer      user_data)
+{
+  GtkMenuTrackerOpener *opener = user_data;
+
+  if (g_str_equal (action_name, opener->submenu_action))
+    gtk_menu_tracker_opener_update (opener);
+}
+
+static void
+gtk_menu_tracker_opener_free (gpointer data)
+{
+  GtkMenuTrackerOpener *opener = data;
+
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_added, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_removed, opener);
+  g_signal_handlers_disconnect_by_func (opener->item->observable, gtk_menu_tracker_opener_changed, opener);
+
+  g_action_group_change_action_state (G_ACTION_GROUP (opener->item->observable),
+                                      opener->submenu_action,
+                                      g_variant_new_boolean (FALSE));
+
+  gtk_menu_tracker_item_set_submenu_shown (opener->item, FALSE);
+
+  g_free (opener->submenu_action);
+
+  g_slice_free (GtkMenuTrackerOpener, opener);
+}
+
+static GtkMenuTrackerOpener *
+gtk_menu_tracker_opener_new (GtkMenuTrackerItem *item,
+                             const gchar        *submenu_action)
+{
+  GtkMenuTrackerOpener *opener;
+
+  opener = g_slice_new (GtkMenuTrackerOpener);
+  opener->first_time = TRUE;
+  opener->item = item;
+
+  if (item->action_namespace)
+    opener->submenu_action = g_strjoin (".", item->action_namespace, submenu_action, NULL);
+  else
+    opener->submenu_action = g_strdup (submenu_action);
+
+  g_signal_connect (item->observable, "action-added", G_CALLBACK (gtk_menu_tracker_opener_added), opener);
+  g_signal_connect (item->observable, "action-removed", G_CALLBACK (gtk_menu_tracker_opener_removed), opener);
+  g_signal_connect (item->observable, "action-state-changed", G_CALLBACK (gtk_menu_tracker_opener_changed), opener);
+
+  gtk_menu_tracker_opener_update (opener);
+
+  return opener;
+}
+
+void
+gtk_menu_tracker_item_request_submenu_shown (GtkMenuTrackerItem *self,
+                                             gboolean            shown)
+{
+  const gchar *submenu_action;
+  gboolean has_submenu_action;
+
+  if (shown == self->submenu_requested)
+    return;
+
+  has_submenu_action = g_menu_item_get_attribute (self->item, "submenu-action", "&s", &submenu_action);
+
+  self->submenu_requested = shown;
+
+  /* If we have a submenu action, start a submenu opener and wait
+   * for the reply from the client. Otherwise, simply open the
+   * submenu immediately.
+   */
+  if (has_submenu_action)
+    {
+      if (shown)
+        g_object_set_data_full (G_OBJECT (self), "submenu-opener",
+                                gtk_menu_tracker_opener_new (self, submenu_action),
+                                gtk_menu_tracker_opener_free);
+      else
+        g_object_set_data (G_OBJECT (self), "submenu-opener", NULL);
+    }
+  else
+    gtk_menu_tracker_item_set_submenu_shown (self, shown);
+}
diff --git a/src/gtkmenutrackeritem.h b/src/gtkmenutrackeritem.h
new file mode 100644 (file)
index 0000000..9db30eb
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2011, 2013 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __GTK_MENU_TRACKER_ITEM_H__
+#define __GTK_MENU_TRACKER_ITEM_H__
+
+#include "gtkactionobservable.h"
+
+#define GTK_TYPE_MENU_TRACKER_ITEM                          (gtk_menu_tracker_item_get_type ())
+#define GTK_MENU_TRACKER_ITEM(inst)                         (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+                                                             GTK_TYPE_MENU_TRACKER_ITEM, GtkMenuTrackerItem))
+#define GTK_IS_MENU_TRACKER_ITEM(inst)                      (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+                                                             GTK_TYPE_MENU_TRACKER_ITEM))
+
+typedef struct _GtkMenuTrackerItem GtkMenuTrackerItem;
+
+#define GTK_TYPE_MENU_TRACKER_ITEM_ROLE                     (gtk_menu_tracker_item_role_get_type ())
+
+typedef enum  {
+  GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
+  GTK_MENU_TRACKER_ITEM_ROLE_CHECK,
+  GTK_MENU_TRACKER_ITEM_ROLE_RADIO,
+} GtkMenuTrackerItemRole;
+
+GType                   gtk_menu_tracker_item_get_type                  (void) G_GNUC_CONST;
+
+GType                   gtk_menu_tracker_item_role_get_type             (void) G_GNUC_CONST;
+
+GtkMenuTrackerItem *   _gtk_menu_tracker_item_new                       (GtkActionObservable *observable,
+                                                                         GMenuModel          *model,
+                                                                         gint                 item_index,
+                                                                         const gchar         *action_namespace,
+                                                                         gboolean             is_separator);
+
+GtkActionObservable *  _gtk_menu_tracker_item_get_observable            (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_is_separator          (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_has_submenu           (GtkMenuTrackerItem *self);
+
+const gchar *           gtk_menu_tracker_item_get_label                 (GtkMenuTrackerItem *self);
+
+GIcon *                 gtk_menu_tracker_item_get_icon                  (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_sensitive             (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_visible               (GtkMenuTrackerItem *self);
+
+GtkMenuTrackerItemRole  gtk_menu_tracker_item_get_role                  (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_toggled               (GtkMenuTrackerItem *self);
+
+const gchar *           gtk_menu_tracker_item_get_accel                 (GtkMenuTrackerItem *self);
+
+GMenuModel *           _gtk_menu_tracker_item_get_submenu               (GtkMenuTrackerItem *self);
+
+gchar *                _gtk_menu_tracker_item_get_submenu_namespace     (GtkMenuTrackerItem *self);
+
+gboolean                gtk_menu_tracker_item_get_should_request_show   (GtkMenuTrackerItem *self);
+
+void                    gtk_menu_tracker_item_activated                 (GtkMenuTrackerItem *self);
+
+void                    gtk_menu_tracker_item_request_submenu_shown     (GtkMenuTrackerItem *self,
+                                                                         gboolean            shown);
+
+gboolean                gtk_menu_tracker_item_get_submenu_shown         (GtkMenuTrackerItem *self);
+
+#endif
index 788605ec4b2af160d0d31171e771178a7785c2ec..ac1c8b23cc54f92052706ebc5e843d67935f13cd 100644 (file)
 #include "shell-menu-tracker.h"
 #include "gtkmenutracker.h"
 
+/**
+ * SECTION:shell-menu-tracker
+ * @short_description: a simple wrapper around #GtkMenuTracker
+ *                     to make it bindable.
+ */
+
 struct _ShellMenuTracker
 {
   guint ref_count;
@@ -40,17 +46,12 @@ struct _ShellMenuTracker
 };
 
 static void
-shell_menu_tracker_insert_func (gint position,
-                                GMenuModel *model,
-                                gint item_index,
-                                const gchar *action_namespace,
-                                gboolean is_separator,
+shell_menu_tracker_insert_func (GtkMenuTrackerItem *item,
+                                gint position,
                                 gpointer user_data)
 {
   ShellMenuTracker *tracker = (ShellMenuTracker *) user_data;
-  tracker->insert_func (position, model, item_index,
-                        action_namespace, is_separator,
-                        tracker->insert_user_data);
+  tracker->insert_func (item, position, tracker->insert_user_data);
 }
 
 static void
@@ -63,6 +64,7 @@ shell_menu_tracker_remove_func (gint position,
 
 /**
  * shell_menu_tracker_new:
+ * @observable:
  * @model:
  * @action_namespace: (allow-none):
  * @insert_func:
@@ -73,7 +75,8 @@ shell_menu_tracker_remove_func (gint position,
  * @remove_notify:
  */
 ShellMenuTracker *
-shell_menu_tracker_new (GMenuModel                 *model,
+shell_menu_tracker_new (GtkActionObservable        *observable,
+                        GMenuModel                 *model,
                         const gchar                *action_namespace,
                         ShellMenuTrackerInsertFunc  insert_func,
                         gpointer                    insert_user_data,
@@ -92,7 +95,8 @@ shell_menu_tracker_new (GMenuModel                 *model,
   tracker->remove_user_data = remove_user_data;
   tracker->remove_notify = remove_notify;
 
-  tracker->tracker = gtk_menu_tracker_new (model,
+  tracker->tracker = gtk_menu_tracker_new (observable,
+                                           model,
                                            TRUE, /* with separators */
                                            action_namespace,
                                            shell_menu_tracker_insert_func,
index 9af602e009e68f8c858f3eebfe4f2d92dbad81f8..df1482a4290d9c8af2838d85b964b193be4fafb8 100644 (file)
 
 #include <gio/gio.h>
 
+#include "gtkmenutrackeritem.h"
+
 typedef struct _ShellMenuTracker ShellMenuTracker;
 
 GType shell_menu_tracker_get_type (void) G_GNUC_CONST;
 
-typedef void         (* ShellMenuTrackerInsertFunc)       (gint                      position,
-                                                           GMenuModel               *model,
-                                                           gint                      item_index,
-                                                           const gchar              *action_namespace,
-                                                           gboolean                  is_separator,
+typedef void         (* ShellMenuTrackerInsertFunc)       (GtkMenuTrackerItem       *item,
+                                                           gint                      position,
                                                            gpointer                  user_data);
 
 typedef void         (* ShellMenuTrackerRemoveFunc)       (gint                      position,
                                                            gpointer                  user_data);
 
-ShellMenuTracker * shell_menu_tracker_new (GMenuModel                 *model,
+ShellMenuTracker * shell_menu_tracker_new (GtkActionObservable        *observable,
+                                           GMenuModel                 *model,
                                            const gchar                *action_namespace,
                                            ShellMenuTrackerInsertFunc  insert_func,
                                            gpointer                    insert_user_data,